using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MonoGame.Extended.Tests.Fixtures;
using MonoGame.Extended.ViewportAdapters;

namespace MonoGame.Extended.Tests;

[Collection("GraphicsTest")]
public sealed class OrthographicCameraTests
{
    private readonly GraphicsTestFixture _graphicsFixture;

    public OrthographicCameraTests(GraphicsTestFixture graphicsTestFixture)
    {
        _graphicsFixture = graphicsTestFixture;
    }

    [Fact]
    public void SetPosition_WorldBoundsDisabled_SetsValueWithoutClamping()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        camera.DisableWorldBounds();
        Vector2 expectedPosition = new Vector2(100, 100);

        camera.Position = expectedPosition;

        Assert.Equal(expectedPosition, camera.Position);
    }

    [Fact]
    public void SetPosition_WorldBoundsEnabled_ClampsToMinimumBounds()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
        Rectangle worldBounds = new Rectangle(0, 0, viewport.Width * 2, viewport.Height * 2);
        camera.EnableWorldBounds(worldBounds);

        camera.Position = new Vector2(-100, -100);

        Vector2 expectedPosition = new Vector2(0, 0);
        Assert.Equal(expectedPosition, camera.Position);
    }

    [Fact]
    public void SetPosition_WorldBoundsEnabled_ClampsToMaximumBounds()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
        Rectangle worldBounds = new Rectangle(0, 0, viewport.Width * 2, viewport.Height * 2);
        camera.EnableWorldBounds(worldBounds);

        camera.Position = new Vector2(viewport.Width, viewport.Height) * 3;

        Vector2 expectedPosition = new Vector2(worldBounds.Right - viewport.Width, worldBounds.Bottom - viewport.Height);
        Assert.Equal(expectedPosition, camera.Position);
    }

    [Fact]
    public void SetPosition_WorldBoundsEnabled_DoesNotClampWhenWithinBounds()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
        Rectangle worldBounds = new Rectangle(0, 0, viewport.Width * 2, viewport.Height * 2);
        camera.EnableWorldBounds(worldBounds);

        Vector2 expectedPosition = new Vector2(viewport.Width, viewport.Height);
        camera.Position = expectedPosition;

        Assert.Equal(expectedPosition, camera.Position);
    }

    [Fact]
    public void SetPosition_WorldBoundsSmallerThanCamera_CentersOnWorldBounds()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        Rectangle worldBounds = new Rectangle(100, 200, 50, 50);
        camera.EnableWorldBounds(worldBounds);

        camera.Position = new Vector2(1000, 1000);

        Vector2 expectedCenter = worldBounds.Center.ToVector2();
        Assert.Equal(expectedCenter, camera.Center);
    }

    [Fact]
    public void SetZoom_DefaultLimits_SetsValueWithoutClamping()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        camera.DisableWorldBounds();
        float expectedZoom = 2.0f;

        camera.Zoom = expectedZoom;

        Assert.Equal(expectedZoom, camera.Zoom);
    }

    [Fact]
    public void SetZoom_BelowMinimumZoom_ClampsToMinimum()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        camera.DisableWorldBounds();
        camera.MinimumZoom = 1.0f;

        camera.Zoom = 0.9f;

        Assert.Equal(camera.MinimumZoom, camera.Zoom);
    }

    [Fact]
    public void SetZoom_AboveMaximumZoom_ClampsToMaximum()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        camera.DisableWorldBounds();
        camera.MaximumZoom = 1.0f;

        camera.Zoom = 1.1f;

        Assert.Equal(camera.MaximumZoom, camera.Zoom);
    }

    [Fact]
    public void SetZoom_WorldBoundsEnabled_BelowMinimumWorldBoundsZoom_ClampsToWorldBounds()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
        Rectangle worldBounds = new Rectangle(0, 0, viewport.Width * 2, viewport.Height * 2);
        camera.EnableWorldBounds(worldBounds);
        camera.IsZoomClampedToWorldBounds = true;

        // Viewport for testing is 800x480, so world bounds are 1600x960
        // Minimum zoom to keep view within bounds: max(800/1600, 480/960) = max(0.5, 0.5) = 0.5
        // So a zoom at 0.5 is at the world bounds minium, so we set lower than that to check clamping.
        camera.Zoom = 0.3f;

        Assert.Equal(0.5f, camera.Zoom);
    }

    [Fact]
    public void SetZoom_WorldBoundsEnabled_AboveMaximumWorldBoundsZoom_ClampsToWorldBounds()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
        Rectangle worldBounds = new Rectangle(0, 0, viewport.Width * 2, viewport.Height * 2);
        camera.EnableWorldBounds(worldBounds);
        camera.IsZoomClampedToWorldBounds = true;

        // Viewport for testing is 800x480, so world bounds are 1600x960
        // Minimum zoom to keep view within bounds: max(800/1600, 480/960) = max(0.5, 0.5) = 0.5
        // So a zoom at 0.5 is at the world bounds minium, so we set lower than that to check clamping.
        camera.Zoom = 0.3f;

        Assert.Equal(0.5f, camera.Zoom);
    }

    [Fact]
    public void SetZoom_ExplicitMaximumZoom_TakesPrecedenceOverWorldBoundsMinimum()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
        Rectangle worldBounds = new Rectangle(0, 0, viewport.Width * 2, viewport.Height * 2);
        camera.EnableWorldBounds(worldBounds);
        camera.IsZoomClampedToWorldBounds = true;

        // Set explicit maximum BELOW what world bounds minimum requires (0.5)
        camera.MaximumZoom = 0.4f;

        // Try to set zoom to world bounds minimum
        camera.Zoom = 0.5f;

        // Explicit MaximumZoom should take precedence
        Assert.Equal(0.4f, camera.Zoom);
    }

    [Fact]
    public void SetZoom_WorldBoundsEnabled_ClampsPositionAfterZoomChange()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
        Rectangle worldBounds = new Rectangle(0, 0, viewport.Width, viewport.Height);

        // Position camera at edge of world bounds
        camera.Position = new Vector2(viewport.Width, viewport.Height);

        camera.EnableWorldBounds(worldBounds);
        camera.IsZoomClampedToWorldBounds = true;

        // Zoom out
        // this should force position adjustment to keep view in bounds
        camera.Zoom = 0.5f;

        // Zoom clamped to 1.0 (camera sees 800×480, same as world bounds)
        Assert.Equal(1.0f, camera.Zoom);

        // With zoom 1.0 and world bounds = viewport size, only valid position is (0, 0)
        Assert.Equal(0f, camera.Position.X, 2);
        Assert.Equal(0f, camera.Position.Y, 2);
    }

    [Fact]
    public void SetMinimumZoom_AboveCurrentZoom_ClampsCurrentZoom()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        camera.Zoom = 0.5f;

        camera.MinimumZoom = 1.0f;

        Assert.Equal(1.0f, camera.Zoom);
    }

    [Fact]
    public void SetMaximumZoom_BelowCurrentZoom_ClampsCurrentZoom()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        camera.Zoom = 2.0f;

        camera.MaximumZoom = 1.0f;

        Assert.Equal(1.0f, camera.Zoom);
    }

    [Fact]
    public void SetMinimumZoom_Negative_ThrowsArgumentOutOfRangeException()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);

        Assert.Throws<ArgumentOutOfRangeException>(() => camera.MinimumZoom = -1.0f);
    }

    [Fact]
    public void SetMaximumZoom_Negative_ThrowsArgumentOutOfRangeException()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);

        Assert.Throws<ArgumentOutOfRangeException>(() => camera.MaximumZoom = -1.0f);
    }

    [Fact]
    public void SetPitch_BelowMinimum_ClampsToMinimum()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        camera.MinimumPitch = 1.0f;

        camera.Pitch = 0.9f;

        Assert.Equal(camera.MinimumPitch, camera.Pitch);
    }

    [Fact]
    public void SetPitch_AboveMaximum_ClampsToMaximum()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        camera.MaximumPitch = 1.0f;

        camera.Pitch = 1.1f;

        Assert.Equal(camera.MaximumPitch, camera.Pitch);
    }

    [Fact]
    public void SetMinimumPitch_Negative_ThrowsArgumentOutOfRangeException()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        Assert.Throws<ArgumentOutOfRangeException>(() => camera.MinimumPitch = -0.01f);
    }

    [Fact]
    public void SetMaximumPitch_Negative_ThrowsArgumentOutOfRangeException()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        Assert.Throws<ArgumentOutOfRangeException>(() => camera.MaximumPitch = -0.01f);
    }

    [Fact]
    public void BoundingRectangle_WithMovement_ReturnsCorrectBounds()
    {
        DefaultViewportAdapter viewportAdapter = new DefaultViewportAdapter(_graphicsFixture.GraphicsDevice);
        OrthographicCamera camera = new OrthographicCamera(viewportAdapter);

        // Move right 2, then down 3
        Vector2 movement = new Vector2(2, 3);
        camera.Move(new Vector2(movement.X, 0));
        camera.Move(new Vector2(0, movement.Y));

        RectangleF boundingRectangle = camera.BoundingRectangle;

        Assert.Equal(movement.X, boundingRectangle.Left, 2);
        Assert.Equal(movement.Y, boundingRectangle.Top, 2);
        Assert.Equal(movement.X + viewportAdapter.VirtualWidth, boundingRectangle.Right, 2);
        Assert.Equal(movement.Y + viewportAdapter.VirtualHeight, boundingRectangle.Bottom, 2);
    }

    [Fact]
    public void BoundingRectangle_WithZoom_ReturnsCorrectBounds()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);

        camera.Zoom = 2f;

        RectangleF boundingRectangle = camera.BoundingRectangle;

        // With 2x zoom on 800x480 viewport, camera sees 400x240 area
        Assert.Equal(400f, boundingRectangle.Width, 2);
        Assert.Equal(240f, boundingRectangle.Height, 2);
    }

    [Fact]
    public void ContainsPoint_WithDefaultCamera_ReturnsCorrectContainment()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;

        Assert.Equal(ContainmentType.Contains, camera.Contains(new Point(1, 1)));
        Assert.Equal(ContainmentType.Contains, camera.Contains(new Point(viewport.Width - 1, viewport.Height - 1)));
        Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Point(-1, -1)));
        Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Point(viewport.Width + 1, viewport.Height + 1)));
    }

    [Fact]
    public void ContainsVector2_WithDefaultCamera_ReturnsCorrectContainment()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;

        Assert.Equal(ContainmentType.Contains, camera.Contains(new Vector2(viewport.Width - 0.5f, viewport.Height - 0.5f)));
        Assert.Equal(ContainmentType.Contains, camera.Contains(new Vector2(0.5f, 0.5f)));
        Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Vector2(-0.5f, -0.5f)));
        Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Vector2(viewport.Width + 0.5f, viewport.Height + 0.5f)));
        Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Vector2(-0.5f, viewport.Height / 2f)));
        Assert.Equal(ContainmentType.Contains, camera.Contains(new Vector2(0.5f, viewport.Height / 2f)));
        Assert.Equal(ContainmentType.Contains, camera.Contains(new Vector2(viewport.Width - 0.5f, viewport.Height / 2f)));
        Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Vector2(viewport.Width + 0.5f, viewport.Height / 2f)));
    }

    [Fact]
    public void ContainsRectangle_WithDefaultCamera_ReturnsCorrectContainment()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);

        Assert.Equal(ContainmentType.Intersects, camera.Contains(new Rectangle(-50, -50, 100, 100)));
        Assert.Equal(ContainmentType.Contains, camera.Contains(new Rectangle(50, 50, 100, 100)));
        Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Rectangle(850, 500, 100, 100)));
    }

    [Fact]
    public void ContainsRectangle_FullyContained_ReturnsContains()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        Rectangle fullyContainedRect = new Rectangle(100, 100, 200, 200);

        ContainmentType result = camera.Contains(fullyContainedRect);

        Assert.Equal(ContainmentType.Contains, result);
    }

    [Fact]
    public void ContainsRectangle_PartiallyContained_ReturnsIntersects()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        Rectangle partiallyContainedRect = new Rectangle(-50, -50, 100, 100);

        ContainmentType result = camera.Contains(partiallyContainedRect);

        Assert.Equal(ContainmentType.Intersects, result);
    }

    [Fact]
    public void EnableWorldBounds_SetsWorldBoundsAndFlag()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        Rectangle worldBounds = new Rectangle(0, 0, 800, 600);

        camera.EnableWorldBounds(worldBounds);

        Assert.Equal(worldBounds, camera.WorldBounds);
        Assert.True(camera.IsClampedToWorldBounds);
    }

    [Fact]
    public void DisableWorldBounds_ClearsWorldBoundsAndFlag()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        camera.EnableWorldBounds(new Rectangle(0, 0, 800, 600));

        camera.DisableWorldBounds();

        Assert.Equal(Rectangle.Empty, camera.WorldBounds);
        Assert.False(camera.IsClampedToWorldBounds);
    }

    [Fact]
    public void Move_WithoutRotation_TranslatesPosition()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        Vector2 originalPosition = camera.Position;
        Vector2 movement = new Vector2(10, 20);

        camera.Move(movement);

        Assert.Equal(originalPosition + movement, camera.Position);
    }

    [Fact]
    public void Move_WithRotation_TranslatesPositionRelativeToRotation()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);

        // 90 degrees
        camera.Rotation = MathHelper.PiOver2;

        // Move right in world space
        Vector2 movement = new Vector2(10, 0);

        camera.Move(movement);

        // With 90 degree rotation, moving "right" should actually move "up" in screen space
        // The movement is transformed by the inverse rotation
        Vector2 expectedMovement = Vector2.Transform(movement, Matrix.CreateRotationZ(-camera.Rotation));

        Assert.Equal(expectedMovement.X, camera.Position.X, 3);
        Assert.Equal(expectedMovement.Y, camera.Position.Y, 3);
    }

    [Fact]
    public void Rotate_IncreasesRotation()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        var deltaRotation = MathHelper.PiOver4;

        camera.Rotate(deltaRotation);

        Assert.Equal(deltaRotation, camera.Rotation, 5);
    }

    [Fact]
    public void ZoomIn_IncreasesZoom()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        float originalZoom = camera.Zoom;

        camera.ZoomIn(1);

        Assert.Equal(originalZoom + 1, camera.Zoom);
    }

    [Fact]
    public void ZoomOut_DecreasesZoom()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        float originalZoom = camera.Zoom;

        camera.ZoomOut(1);

        Assert.Equal(originalZoom - 1, camera.Zoom);
    }

    [Fact]
    public void PitchUp_IncreasesPitch()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        float originalPitch = camera.Pitch;

        camera.PitchUp(1);

        Assert.Equal(originalPitch + 1, camera.Pitch);
    }

    [Fact]
    public void PitchDown_DecreasesPitch()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        float originalPitch = camera.Pitch;

        camera.PitchDown(1);

        Assert.Equal(originalPitch - 1, camera.Pitch);
    }

    [Fact]
    public void LookAt_SetsPositionCorrectly()
    {
        DefaultViewportAdapter viewportAdapter = new DefaultViewportAdapter(_graphicsFixture.GraphicsDevice);
        OrthographicCamera camera = new OrthographicCamera(viewportAdapter);
        Vector2 targetPosition = new Vector2(100, 200);

        camera.LookAt(targetPosition);

        Vector2 expectedPosition = targetPosition - new Vector2(viewportAdapter.VirtualWidth, viewportAdapter.VirtualHeight) * 0.5f;
        Assert.Equal(expectedPosition, camera.Position);
    }

    [Fact]
    public void ScreenToWorld_WithCameraMovement_TransformsCorrectly()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        camera.Position = new Vector2(100, 200);

        // Screen position at origin
        Vector2 screenPosition = Vector2.Zero;
        Vector2 worldPosition = camera.ScreenToWorld(screenPosition);

        // World position should account for camera offset
        Assert.Equal(100, worldPosition.X, 2);
        Assert.Equal(200, worldPosition.Y, 2);
    }

    [Fact]
    public void WorldToScreen_WithCameraMovement_TransformsCorrectly()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        camera.Position = new Vector2(100, 200);

        // World position at camera position
        Vector2 worldPosition = new Vector2(100, 200);
        Vector2 screenPosition = camera.WorldToScreen(worldPosition);

        // Should appear at screen origin
        Assert.Equal(0, screenPosition.X, 2);
        Assert.Equal(0, screenPosition.Y, 2);
    }

    [Fact]
    public void ScreenToWorld_WithZoom_TransformsCorrectly()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        camera.Zoom = 2.0f;

        Vector2 screenPosition = new Vector2(100, 100);
        Vector2 worldPosition = camera.ScreenToWorld(screenPosition);

        // With default camera:
        // - Origin is at (400, 240) - the viewport center
        // - Position is at (0, 0)
        // - Screen (100, 100) is 300px left and 140px up from Origin
        // - With 2x zoom: world offset is (300/2, 140/2) = (150, 70) from Origin
        // - World position: Origin - offset = (400 - 150, 240 - 70) = (250, 170)
        Assert.Equal(250, worldPosition.X, 2);
        Assert.Equal(170, worldPosition.Y, 2);
    }

    [Fact]
    public void WorldToScreen_WithZoom_TransformsCorrectly()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        camera.Zoom = 2.0f;

        Vector2 worldPosition = new Vector2(100, 100);
        Vector2 screenPosition = camera.WorldToScreen(worldPosition);

        // With default camera:
        // - Origin is at (400, 240) - the viewport center
        // - Position is at (0, 0)
        // - Camera.Center is at (400, 240)
        // - World (100, 100) is 300px left and 140px up from Camera.Center
        // - With 2x zoom: screen offset is (300 * 2, 140 * 2) = (600, 280) from Origin
        // - Screen position: Origin - offset = (400 - 600, 240 - 280) = (-200, -40)
        Assert.Equal(-200, screenPosition.X, 2);
        Assert.Equal(-40, screenPosition.Y, 2);
    }

    [Fact]
    public void WorldToScreen_RoundTrip_ReturnsOriginalPosition()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        camera.Position = new Vector2(100, 200);
        camera.Zoom = 1.5f;

        Vector2 originalWorld = new Vector2(250, 350);

        Vector2 screen = camera.WorldToScreen(originalWorld);
        Vector2 backToWorld = camera.ScreenToWorld(screen);

        Assert.Equal(originalWorld.X, backToWorld.X, 2);
        Assert.Equal(originalWorld.Y, backToWorld.Y, 2);
    }

    [Fact]
    public void GetViewMatrix_ReturnsValidMatrix()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);

        Matrix viewMatrix = camera.GetViewMatrix();

        Assert.Equal(Matrix.Identity, viewMatrix);
    }

    [Fact]
    public void GetInverseViewMatrix_IsInverseOfViewMatrix()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        camera.Position = new Vector2(10, 20);
        camera.Rotation = MathHelper.PiOver4;
        camera.Zoom = 2f;

        Matrix viewMatrix = camera.GetViewMatrix();
        Matrix inverseViewMatrix = camera.GetInverseViewMatrix();
        Matrix shouldBeIdentity = Matrix.Multiply(viewMatrix, inverseViewMatrix);

        // Check if the result is close to identity matrix
        AssertExtensions.Equal(Matrix.Identity, shouldBeIdentity, 3);
    }

    [Fact]
    public void GetViewMatrix_WithParallaxFactor_ReturnsValidMatrix()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);

        // Set non-zero position to make parallax effect visible
        camera.Position = new Vector2(100, 50);
        Vector2 parallaxFactor = new Vector2(0.5f, 0.5f);

        Matrix viewMatrix = camera.GetViewMatrix(parallaxFactor);

        Assert.NotEqual(Matrix.Identity, viewMatrix);
    }

    [Fact]
    public void GetViewMatrix_WithParallaxFactor_AppliesCorrectTransformation()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        camera.Position = new Vector2(100, 60);
        Vector2 parallaxFactor = new Vector2(0.5f, 0.25f);

        Matrix parallaxMatrix = camera.GetViewMatrix(parallaxFactor);

        // Default parallax factor of (1,1)
        Matrix normalMatrix = camera.GetViewMatrix();

        // The matrices should be different when parallax factor is not (1,1) and position is not zero
        Assert.NotEqual(normalMatrix, parallaxMatrix);
        Assert.NotEqual(Matrix.Identity, parallaxMatrix);
        Assert.NotEqual(Matrix.Identity, normalMatrix);

        // With position (100, 60) and parallax (0.5, 0.25):
        // Expected translation = -(100 * 0.5, 60 * 0.25) = (-50, -15)
        // This should be reflected in the view matrix M41, M42 values
        Assert.Equal(-50f, parallaxMatrix.M41, 1);
        Assert.Equal(-15f, parallaxMatrix.M42, 1);
    }

    [Fact]
    public void GetBoundingFrustum_ReturnsValidFrustum()
    {
        var camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;

        BoundingFrustum boundingFrustum = camera.GetBoundingFrustum();
        Vector3[] corners = boundingFrustum.GetCorners();

        // Verify we have 8 corners (standard frustum)
        Assert.Equal(8, corners.Length);

        // Check near plane corners (Z = 1)
        Assert.Equal(0, corners[0].X, 2);
        Assert.Equal(0, corners[0].Y, 2);
        Assert.Equal(1, corners[0].Z, 2);

        Assert.Equal(viewport.Width, corners[1].X, 2);
        Assert.Equal(0, corners[1].Y, 2);
        Assert.Equal(1, corners[1].Z, 2);

        Assert.Equal(viewport.Width, corners[2].X, 2);
        Assert.Equal(viewport.Height, corners[2].Y, 2);
        Assert.Equal(1, corners[2].Z, 2);

        Assert.Equal(0, corners[3].X, 2);
        Assert.Equal(viewport.Height, corners[3].Y, 2);
        Assert.Equal(1, corners[3].Z, 2);

        // Check far plane corners (Z = 0)
        Assert.Equal(0, corners[4].X, 2);
        Assert.Equal(0, corners[4].Y, 2);
        Assert.Equal(0, corners[4].Z, 2);

        Assert.Equal(viewport.Width, corners[5].X, 2);
        Assert.Equal(0, corners[5].Y, 2);
        Assert.Equal(0, corners[5].Z, 2);

        Assert.Equal(viewport.Width, corners[6].X, 2);
        Assert.Equal(viewport.Height, corners[6].Y, 2);
        Assert.Equal(0, corners[6].Z, 2);

        Assert.Equal(0, corners[7].X, 2);
        Assert.Equal(viewport.Height, corners[7].Y, 2);
        Assert.Equal(0, corners[7].Z, 2);
    }

    [Fact]
    public void ZoomIn_WithZoomCenter_KeepsZoomCenterFixedOnScreen()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        camera.Position = Vector2.Zero;
        camera.Zoom = 1.0f;

        Vector2 zoomCenter = new Vector2(100, 100);
        Vector2 screenBefore = camera.WorldToScreen(zoomCenter);

        camera.ZoomIn(0.5f, zoomCenter);
        Vector2 screenAfter = camera.WorldToScreen(zoomCenter);

        Assert.Equal(screenBefore.X, screenAfter.X, 1);
        Assert.Equal(screenBefore.Y, screenAfter.Y, 1);
    }

    [Fact]
    public void ZoomOut_WithZoomCenter_KeepsZoomCenterFixedOnScreen()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        camera.Position = Vector2.Zero;
        camera.Zoom = 2.0f;

        Vector2 zoomCenter = new Vector2(100, 100);
        Vector2 screenBefore = camera.WorldToScreen(zoomCenter);

        camera.ZoomOut(0.5f, zoomCenter);
        Vector2 screenAfter = camera.WorldToScreen(zoomCenter);

        Assert.Equal(screenBefore.X, screenAfter.X, 1);
        Assert.Equal(screenBefore.Y, screenAfter.Y, 1);
    }

    [Fact]
    public void ZoomIn_WithZoomCenter_ClampedByMinimumZoom_DoesNotAdjustPosition()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        camera.MaximumZoom = 2.0f;
        camera.Zoom = camera.MaximumZoom;
        camera.Position = new Vector2(50, 50);

        Vector2 zoomCenter = new Vector2(100, 100);
        Vector2 positionBefore = camera.Position;

        camera.ZoomIn(1.0f, zoomCenter);

        Assert.Equal(2.0f, camera.Zoom);
        Assert.Equal(positionBefore, camera.Position);
    }

    [Fact]
    public void ZoomOut_WithZoomCenter_ClampedByMinimumZoom_DoesNotAdjustPosition()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        camera.MinimumZoom = 0.5f;
        camera.Zoom = camera.MinimumZoom;
        camera.Position = new Vector2(50, 50);

        Vector2 zoomCenter = new Vector2(100, 100);
        Vector2 positionBefore = camera.Position;

        camera.ZoomOut(1.0f, zoomCenter);

        Assert.Equal(0.5f, camera.Zoom);
        Assert.Equal(positionBefore, camera.Position);
    }


    [Fact]
    public void ZoomIn_WithZoomCenter_AtOrigin_AdjustsPositionCorrectly()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);

        camera.Position = new Vector2(100, 100);
        camera.Zoom = 1.0f;

        Vector2 zoomCenter = camera.Origin;
        Vector2 screenBefore = camera.WorldToScreen(zoomCenter);

        camera.ZoomIn(0.5f, zoomCenter);
        Vector2 screenAfter = camera.WorldToScreen(zoomCenter);

        Assert.Equal(screenBefore.X, screenAfter.X, 1);
        Assert.Equal(screenBefore.Y, screenAfter.Y, 1);
    }

    [Fact]
    public void ZoomOut_WithZoomCenter_AtOrigin_AdjustsPositionCorrectly()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);

        camera.Position = new Vector2(100, 100);
        camera.Zoom = 2.0f;

        Vector2 zoomCenter = camera.Origin;
        Vector2 screenBefore = camera.WorldToScreen(zoomCenter);

        camera.ZoomOut(0.5f, zoomCenter);
        Vector2 screenAfter = camera.WorldToScreen(zoomCenter);

        Assert.Equal(screenBefore.X, screenAfter.X, 1);
        Assert.Equal(screenBefore.Y, screenAfter.Y, 1);
    }

    [Fact]
    public void ZoomIn_WithZoomCenter_WorldBoundsEnabled_RespectsBounds()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
        Rectangle worldBounds = new Rectangle(0, 0, viewport.Width * 2, viewport.Height * 2);

        camera.EnableWorldBounds(worldBounds);
        camera.Position = new Vector2(viewport.Width / 2, viewport.Height / 2);
        camera.Zoom = 1.0f;

        Vector2 zoomCenter = new Vector2(200, 200);

        camera.ZoomIn(0.5f, zoomCenter);

        Assert.True(camera.Position.X >= 0);
        Assert.True(camera.Position.Y >= 0);
        Assert.True(camera.Position.X <= worldBounds.Right - viewport.Width / camera.Zoom);
        Assert.True(camera.Position.Y <= worldBounds.Bottom - viewport.Height / camera.Zoom);
    }

    [Fact]
    public void ZoomOut_WithZoomCenter_WorldBoundsEnabled_RespectsBounds()
    {
        OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
        Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
        Rectangle worldBounds = new Rectangle(0, 0, viewport.Width * 2, viewport.Height * 2);

        camera.EnableWorldBounds(worldBounds);
        camera.Position = new Vector2(viewport.Width / 2, viewport.Height / 2);
        camera.Zoom = 2.0f;

        Vector2 zoomCenter = new Vector2(200, 200);

        camera.ZoomOut(0.5f, zoomCenter);

        Assert.True(camera.Position.X >= 0);
        Assert.True(camera.Position.Y >= 0);
        Assert.True(camera.Position.X <= worldBounds.Right - viewport.Width / camera.Zoom);
        Assert.True(camera.Position.Y <= worldBounds.Bottom - viewport.Height / camera.Zoom);
    }

    // -----------------------------------------------------------------------------
    // Tests for issue #793
    // OrthographicCamera.ScreenToWorld and similar methods interact poorly with
    // window not at (0,0)
    // https://github.com/MonoGame-Extended/Monogame-Extended/issues/793
    // -----------------------------------------------------------------------------
    //
    // The goal of these tests is to verify that coordinate transformations between
    // world-space and screen-space behave correctly when the viewport has a non-zero
    // origin (e.g. the window is offset or letterboxed).
    //
    // Specifically:
    // - DefaultViewportAdapter: viewport offset should NOT affect transformations.
    // - ScalingViewportAdapter/BoxingViewportAdapter:  offset IS part of transformation.
    //
    // Each test ensures that ScreenToWorld() and WorldToScreen() remain consistent
    // inverses of one another under these different scenarios.
    //
    [Trait("Issue", "#793")]
    [Collection("GraphicsTest")]
    public class OrthographicCameraIssue793Tests
    {
        private readonly GraphicsTestFixture _graphicsFixture;

        public OrthographicCameraIssue793Tests(GraphicsTestFixture graphicsTestFixture)
        {
            _graphicsFixture = graphicsTestFixture;
        }

        // -------------------------------------------------------------------------
        // Test 1: DefaultViewportAdapter (non-scaling)
        // -------------------------------------------------------------------------
        // Verifies that for a simple viewport offset (e.g., window not at (0,0)),
        // the ScreenToWorld transformation ignores viewport origin and maps directly.
        // Mouse/touch input coordinates (from MouseState) are already window-relative,
        // so no offset adjustment should occur.
        [Fact]
        public void ScreenToWorld_WithNonZeroViewportOrigin_TransformsCorrectly()
        {
            Viewport originalViewport = _graphicsFixture.GraphicsDevice.Viewport;

            try
            {
                _graphicsFixture.GraphicsDevice.Viewport = new Viewport(100, 50, 800, 480);

                OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);

                Vector2 screenPosition = new Vector2(200, 150);
                Vector2 worldPosition = camera.ScreenToWorld(screenPosition);

                // Expectation:
                // With viewport origin offset, but no scaling or zoom,
                // the mapping should remain 1:1 with window coordinates.
                Assert.Equal(200, worldPosition.X, 2);
                Assert.Equal(150, worldPosition.Y, 2);
            }
            finally
            {
                _graphicsFixture.GraphicsDevice.Viewport = originalViewport;
            }
        }

        // -------------------------------------------------------------------------
        // Test 2: Round-trip transformation with DefaultViewportAdapter
        // -------------------------------------------------------------------------
        // Ensures that WorldToScreen() and ScreenToWorld() are true inverses even when
        // the viewport has a non-zero origin. The result after round-tripping should
        // return to the original world position (within floating-point tolerance).
        [Fact]
        public void WorldToScreen_RoundTrip_WithNonZeroViewportOrigin_ReturnsOriginalPosition()
        {
            Viewport originalViewport = _graphicsFixture.GraphicsDevice.Viewport;

            try
            {
                _graphicsFixture.GraphicsDevice.Viewport = new Viewport(100, 50, 800, 480);

                OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
                camera.Position = new Vector2(100, 200);
                camera.Zoom = 1.5f;

                Vector2 originalWorld = new Vector2(250, 350);

                Vector2 screen = camera.WorldToScreen(originalWorld);
                Vector2 backToWorld = camera.ScreenToWorld(screen);

                Assert.Equal(originalWorld.X, backToWorld.X, 2);
                Assert.Equal(originalWorld.Y, backToWorld.Y, 2);
            }
            finally
            {
                _graphicsFixture.GraphicsDevice.Viewport = originalViewport;
            }
        }

        // -------------------------------------------------------------------------
        // Test 3: BoxingViewportAdapter (scaling)
        // -------------------------------------------------------------------------
        // Verifies that when using a scaling viewport adapter (BoxingViewportAdapter),
        // ScreenToWorld() correctly accounts for the viewport offset, which defines
        // where the virtual coordinate system is drawn inside the window.
        //
        // In this case, the viewport offset IS part of the coordinate transformation.
        [Fact]
        public void ScreenToWorld_WithBoxingViewportAdapter_TransformsCorrectly()
        {
            Viewport originalViewport = _graphicsFixture.GraphicsDevice.Viewport;

            try
            {
                int virtualWidth = 400;
                int virtualHeight = 240;

                // Create a boxing viewport adapter with a virtual resolution smaller
                // than the actual window. This will introduce a viewport offset
                BoxingViewportAdapter viewportAdapter = new BoxingViewportAdapter(
                    _graphicsFixture.Game.Window,
                    _graphicsFixture.GraphicsDevice,
                    virtualWidth,
                    virtualHeight);

                // Forces recalculation of the viewport region inside the window.
                viewportAdapter.Reset();

                OrthographicCamera camera = new OrthographicCamera(viewportAdapter);

                // Retrieve the actual viewport dimensions and offset
                Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
                float scaleX = (float)viewport.Width / virtualWidth;
                float scaleY = (float)viewport.Height / virtualHeight;

                // The center of the viewport in window space
                Vector2 windowPosition = new Vector2(viewport.X + viewport.Width * 0.5f,
                                                     viewport.Y + viewport.Height * 0.5f);


                Vector2 worldPosition = camera.ScreenToWorld(windowPosition);

                // Should map to center of virtual space (200, 120)
                Assert.Equal(virtualWidth * 0.5f, worldPosition.X, 2);
                Assert.Equal(virtualHeight * 0.5f, worldPosition.Y, 2);
            }
            finally
            {
                _graphicsFixture.GraphicsDevice.Viewport = originalViewport;
            }
        }

        // -------------------------------------------------------------------------
        // Test 4: Round-trip transformation with BoxingViewportAdapter
        // -------------------------------------------------------------------------
        // Ensures that WorldToScreen() and ScreenToWorld() remain perfect inverses
        // when scaling and offset are both involved.
        //
        // This confirms that the viewport offset and scaling transformations
        // are correctly applied and undone in opposite order.
        [Fact]
        public void WorldToScreen_RoundTrip_WithBoxingViewportAdapter_ReturnsOriginalPosition()
        {
            Viewport originalViewport = _graphicsFixture.GraphicsDevice.Viewport;

            try
            {
                int virtualWidth = 400;
                int virtualHeight = 240;

                BoxingViewportAdapter viewportAdapter = new BoxingViewportAdapter(_graphicsFixture.Game.Window,
                                                                                  _graphicsFixture.GraphicsDevice,
                                                                                  virtualWidth,
                                                                                  virtualHeight);

                viewportAdapter.Reset();

                OrthographicCamera camera = new OrthographicCamera(viewportAdapter);
                camera.Position = new Vector2(50, 100);
                camera.Zoom = 1.5f;

                Vector2 originalWorld = new Vector2(125, 175);

                // Round trip world -> screen -> world
                Vector2 screen = camera.WorldToScreen(originalWorld);
                Vector2 backToWorld = camera.ScreenToWorld(screen);

                // Expect round-trip consistency
                Assert.Equal(originalWorld.X, backToWorld.X, 2);
                Assert.Equal(originalWorld.Y, backToWorld.Y, 2);
            }
            finally
            {
                _graphicsFixture.GraphicsDevice.Viewport = originalViewport;
            }
        }
    }

}
